// /Copyright 2003-2005 Arthur van Hoff, Rick Blair
// Licensed under Apache License version 2.0
// Original license LGPL
package javax.jmdns.impl;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import android.util.Log;

/** Parse an incoming DNS message into its components.
 * @version %I%, %G%
 * @author Arthur van Hoff, Werner Randelshofer, Pierre Frisch, Daniel Bobbert */
public final class DNSIncoming {
	public final static String TAG = DNSIncoming.class.toString();
	// This is a hack to handle a bug in the BonjourConformanceTest
	// It is sending out target strings that don't follow the "domain name"
	// format.
	public static boolean USE_DOMAIN_NAME_FORMAT_FOR_SRV_TARGET = true;
	// Implementation note: This vector should be immutable.
	// If a client of DNSIncoming changes the contents of this vector,
	// we get undesired results. To fix this, we have to migrate to
	// the Collections API of Java 1.2. i.e we replace Vector by List.
	// final static Vector EMPTY = new Vector();
	private DatagramPacket packet;
	private int off;
	private int len;
	private byte data[];
	int id;
	private int flags;
	private int numQuestions;
	int numAnswers;
	private int numAuthorities;
	private int numAdditionals;
	private long receivedTime;
	@SuppressWarnings("rawtypes")
	private List questions;
	@SuppressWarnings("rawtypes")
	List answers;

	/** Parse a message from a datagram packet. */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	DNSIncoming(DatagramPacket packet) throws IOException {
		this.packet = packet;
		InetAddress source = packet.getAddress();
		this.data = packet.getData();
		this.len = packet.getLength();
		this.off = packet.getOffset();
		this.questions = Collections.EMPTY_LIST;
		this.answers = Collections.EMPTY_LIST;
		this.receivedTime = System.currentTimeMillis();
		try {
			id = readUnsignedShort();
			flags = readUnsignedShort();
			numQuestions = readUnsignedShort();
			numAnswers = readUnsignedShort();
			numAuthorities = readUnsignedShort();
			numAdditionals = readUnsignedShort();
			// parse questions
			if (numQuestions > 0) {
				questions = Collections.synchronizedList(new ArrayList(
						numQuestions));
				for (int i = 0; i < numQuestions; i++) {
					DNSQuestion question = new DNSQuestion(readName(),
							readUnsignedShort(), readUnsignedShort());
					questions.add(question);
				}
			}
			// parse answers
			int n = numAnswers + numAuthorities + numAdditionals;
			if (n > 0) {
				answers = Collections.synchronizedList(new ArrayList(n));
				for (int i = 0; i < n; i++) {
					String domain = readName();
					int type = readUnsignedShort();
					int clazz = readUnsignedShort();
					int ttl = readInt();
					int len = readUnsignedShort();
					int end = off + len;
					DNSRecord rec = null;
					switch (type) {
					case DNSConstants.TYPE_A: // IPv4
					case DNSConstants.TYPE_AAAA: // IPv6 FIXME [PJYF Oct 14
													// 2004]
													// This has not been tested
						rec = new DNSRecord.Address(domain, type, clazz, ttl,
								readBytes(off, len));
						break;
					case DNSConstants.TYPE_CNAME:
					case DNSConstants.TYPE_PTR:
						String service = "";
						try {
							service = readName();
						} catch (IOException e) {
							// there was a problem reading the service name
							e.printStackTrace();
						}
						rec = new DNSRecord.Pointer(domain, type, clazz, ttl,
								service);
						break;
					case DNSConstants.TYPE_TXT:
						rec = new DNSRecord.Text(domain, type, clazz, ttl,
								readBytes(off, len));
						break;
					case DNSConstants.TYPE_SRV:
						int priority = readUnsignedShort();
						int weight = readUnsignedShort();
						int port = readUnsignedShort();
						String target = "";
						try {
							// This is a hack to handle a bug in the
							// BonjourConformanceTest
							// It is sending out target strings that don't
							// follow the
							// "domain name"
							// format.
							if (USE_DOMAIN_NAME_FORMAT_FOR_SRV_TARGET) {
								target = readName();
							} else {
								target = readNonNameString();
							}
						} catch (IOException e) {
							// this can happen if the type of the label
							// cannot be handled.
							// down below the offset gets advanced to the end
							// of the record
							e.printStackTrace();
						}
						rec = new DNSRecord.Service(domain, type, clazz, ttl,
								priority, weight, port, target);
						break;
					case DNSConstants.TYPE_HINFO:
						// Maybe we should do something with those
						break;
					default:
						break;
					}
					if (rec != null) {
						rec.setRecordSource(source);
						// Add a record, if we were able to create one.
						answers.add(rec);
					} else {
						// Addjust the numbers for the skipped record
						if (answers.size() < numAnswers) {
							numAnswers--;
						} else {
							if (answers.size() < numAnswers + numAuthorities) {
								numAuthorities--;
							} else {
								if (answers.size() < numAnswers
										+ numAuthorities + numAdditionals) {
									numAdditionals--;
								}
							}
						}
					}
					off = end;
				}
			}
		} catch (IOException e) {
			Log.d(TAG, "DNSIncoming() dump " + print(true) + "\n exception ");
			throw e;
		}
	}

	/** Check if the message is a query. */
	boolean isQuery() {
		return (flags & DNSConstants.FLAGS_QR_MASK) == DNSConstants.FLAGS_QR_QUERY;
	}

	/** Check if the message is truncated. */
	public boolean isTruncated() {
		return (flags & DNSConstants.FLAGS_TC) != 0;
	}

	/** Check if the message is a response. */
	boolean isResponse() {
		return (flags & DNSConstants.FLAGS_QR_MASK) == DNSConstants.FLAGS_QR_RESPONSE;
	}

	private int get(int off) throws IOException {
		if ((off < 0) || (off >= len)) {
			throw new IOException("parser error: offset=" + off);
		}
		return data[off] & 0xFF;
	}

	private int readUnsignedShort() throws IOException {
		return (get(off++) << 8) + get(off++);
	}

	private int readInt() throws IOException {
		return (readUnsignedShort() << 16) + readUnsignedShort();
	}

	private byte[] readBytes(int off, int len) throws IOException {
		byte bytes[] = new byte[len];
		System.arraycopy(data, off, bytes, 0, len);
		return bytes;
	}

	private void readUTF(StringBuffer buf, int off, int len) throws IOException {
		for (int end = off + len; off < end;) {
			int ch = get(off++);
			switch (ch >> 4) {
			case 0:
			case 1:
			case 2:
			case 3:
			case 4:
			case 5:
			case 6:
			case 7:
				// 0xxxxxxx
				break;
			case 12:
			case 13:
				// 110x xxxx 10xx xxxx
				ch = ((ch & 0x1F) << 6) | (get(off++) & 0x3F);
				break;
			case 14:
				// 1110 xxxx 10xx xxxx 10xx xxxx
				ch = ((ch & 0x0f) << 12) | ((get(off++) & 0x3F) << 6)
						| (get(off++) & 0x3F);
				break;
			default:
				// 10xx xxxx, 1111 xxxx
				ch = ((ch & 0x3F) << 4) | (get(off++) & 0x0f);
				break;
			}
			buf.append((char) ch);
		}
	}

	private String readNonNameString() throws IOException {
		StringBuffer buf = new StringBuffer();
		int off = this.off;
		int len = get(off++);
		readUTF(buf, off, len);
		return buf.toString();
	}

	private String readName() throws IOException {
		StringBuffer buf = new StringBuffer();
		int off = this.off;
		int next = -1;
		int first = off;
		while (true) {
			int len = get(off++);
			if (len == 0) {
				break;
			}
			switch (len & 0xC0) {
			case 0x00:
				// buf.append("[" + off + "]");
				readUTF(buf, off, len);
				off += len;
				buf.append('.');
				break;
			case 0xC0:
				// buf.append("<" + (off - 1) + ">");
				if (next < 0) {
					next = off + 1;
				}
				off = ((len & 0x3F) << 8) | get(off++);
				if (off >= first) {
					throw new IOException(
							"bad domain name: possible circular name detected."
									+ " name start: " + first
									+ " bad offset: 0x"
									+ Integer.toHexString(off));
				}
				first = off;
				break;
			default:
				throw new IOException("unsupported dns label type: '"
						+ Integer.toHexString(len & 0xC0) + "' at " + (off - 1));
			}
		}
		this.off = (next >= 0) ? next : off;
		return buf.toString();
	}

	/** Debugging. */
	@SuppressWarnings("rawtypes")
	String print(boolean dump) {
		StringBuffer buf = new StringBuffer();
		buf.append(toString() + "\n");
		for (Iterator iterator = questions.iterator(); iterator.hasNext();) {
			buf.append("    ques:" + iterator.next() + "\n");
		}
		int count = 0;
		for (Iterator iterator = answers.iterator(); iterator.hasNext(); count++) {
			if (count < numAnswers) {
				buf.append("    answ:");
			} else {
				if (count < numAnswers + numAuthorities) {
					buf.append("    auth:");
				} else {
					buf.append("    addi:");
				}
			}
			buf.append(iterator.next() + "\n");
		}
		if (dump) {
			for (int off = 0, len = packet.getLength(); off < len; off += 32) {
				int n = Math.min(32, len - off);
				if (off < 10) {
					buf.append(' ');
				}
				if (off < 100) {
					buf.append(' ');
				}
				buf.append(off);
				buf.append(':');
				for (int i = 0; i < n; i++) {
					if ((i % 8) == 0) {
						buf.append(' ');
					}
					buf.append(Integer.toHexString((data[off + i] & 0xF0) >> 4));
					buf.append(Integer.toHexString((data[off + i] & 0x0F) >> 0));
				}
				buf.append("\n");
				buf.append("    ");
				for (int i = 0; i < n; i++) {
					if ((i % 8) == 0) {
						buf.append(' ');
					}
					buf.append(' ');
					int ch = data[off + i] & 0xFF;
					buf.append(((ch > ' ') && (ch < 127)) ? (char) ch : '.');
				}
				buf.append("\n");
				// limit message size
				if (off + 32 >= 256) {
					buf.append("....\n");
					break;
				}
			}
		}
		return buf.toString();
	}

	public String toString() {
		StringBuffer buf = new StringBuffer();
		buf.append(isQuery() ? "dns[query," : "dns[response,");
		if (packet.getAddress() != null) {
			buf.append(packet.getAddress().getHostAddress());
		}
		buf.append(':');
		buf.append(packet.getPort());
		buf.append(",len=");
		buf.append(packet.getLength());
		buf.append(",id=0x");
		buf.append(Integer.toHexString(id));
		if (flags != 0) {
			buf.append(",flags=0x");
			buf.append(Integer.toHexString(flags));
			if ((flags & DNSConstants.FLAGS_QR_RESPONSE) != 0) {
				buf.append(":r");
			}
			if ((flags & DNSConstants.FLAGS_AA) != 0) {
				buf.append(":aa");
			}
			if ((flags & DNSConstants.FLAGS_TC) != 0) {
				buf.append(":tc");
			}
		}
		if (numQuestions > 0) {
			buf.append(",questions=");
			buf.append(numQuestions);
		}
		if (numAnswers > 0) {
			buf.append(",answers=");
			buf.append(numAnswers);
		}
		if (numAuthorities > 0) {
			buf.append(",authorities=");
			buf.append(numAuthorities);
		}
		if (numAdditionals > 0) {
			buf.append(",additionals=");
			buf.append(numAdditionals);
		}
		buf.append("]");
		return buf.toString();
	}

	/** Appends answers to this Incoming.
	 * @throws IllegalArgumentException
	 * If not a query or if Truncated. */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	void append(DNSIncoming that) {
		if (this.isQuery() && this.isTruncated() && that.isQuery()) {
			if (that.numQuestions > 0) {
				if (Collections.EMPTY_LIST.equals(this.questions))
					this.questions = Collections
							.synchronizedList(new ArrayList(that.numQuestions));
				this.questions.addAll(that.questions);
				this.numQuestions += that.numQuestions;
			}
			if (Collections.EMPTY_LIST.equals(answers)) {
				answers = Collections.synchronizedList(new ArrayList());
			}
			if (that.numAnswers > 0) {
				this.answers.addAll(this.numAnswers,
						that.answers.subList(0, that.numAnswers));
				this.numAnswers += that.numAnswers;
			}
			if (that.numAuthorities > 0) {
				this.answers.addAll(
						this.numAnswers + this.numAuthorities,
						that.answers.subList(that.numAnswers, that.numAnswers
								+ that.numAuthorities));
				this.numAuthorities += that.numAuthorities;
			}
			if (that.numAdditionals > 0) {
				this.answers.addAll(that.answers.subList(that.numAnswers
						+ that.numAuthorities, that.numAnswers
						+ that.numAuthorities + that.numAdditionals));
				this.numAdditionals += that.numAdditionals;
			}
		} else {
			throw new IllegalArgumentException();
		}
	}

	public int elapseSinceArrival() {
		return (int) (System.currentTimeMillis() - receivedTime);
	}

	@SuppressWarnings("rawtypes")
	public List getQuestions() {
		return questions;
	}

	@SuppressWarnings("rawtypes")
	public List getAnswers() {
		return answers;
	}
}
